Skip to content

refactor(mocks): use file-scoped types for generated implementation details#5369

Merged
thomhurst merged 1 commit intomainfrom
refactor/mocks-file-scoped-generated-types
Apr 4, 2026
Merged

refactor(mocks): use file-scoped types for generated implementation details#5369
thomhurst merged 1 commit intomainfrom
refactor/mocks-file-scoped-generated-types

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

  • Change MockImpl, MockFactory, and DelegateFactory generated types from internal to file scoped, removing them from user IntelliSense entirely
  • Merge MockImpl + MockFactory into a single generated file (_MockImplFactory.g.cs) so both can be file scoped while still referencing each other
  • Update MockStaticExtension to route through Mock.Of<T>() instead of directly calling the now-invisible factory
  • Extract shared GenerateImplFactoryMembersAndEvents() helper in MockGenerator to reduce duplication

Types now file-scoped (invisible to users)

Type Generated File
{Name}MockImpl _MockImplFactory.g.cs
{Name}WrapMockImpl _MockImplFactory.g.cs
{Name}MockFactory / WrapMockFactory / PartialMockFactory _MockImplFactory.g.cs
{Name}MockDelegateFactory _MockDelegateFactory.g.cs

Not changed (with reasoning)

  • StaticEngine — shared AsyncLocal state referenced across _MockBridge.g.cs and _MockImplFactory.g.cs; must remain internal (already has [EditorBrowsable(Never)])
  • All public user-facing types (Mock wrapper, MockMemberExtensions, MockCall, MockEvents, MockStaticExtension, Mockable bridge) — part of the fluent API surface

Test plan

  • Source generator project builds cleanly (0 errors, 0 warnings)
  • All 25 snapshot tests pass with updated .verified.txt files
  • All 746 TUnit.Mocks.Tests pass on net10.0

…etails

Change MockImpl, MockFactory, and DelegateFactory generated types from
`internal` to `file` scoped, keeping them out of user IntelliSense.
Merge MockImpl + MockFactory into a single generated file so they can
reference each other while both being file-scoped. Update the static
extension to use Mock.Of<T> instead of direct factory calls.
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: refactor(mocks): use file-scoped types for generated implementation details

This is a well-executed, focused refactor. The goal is clear and the implementation is consistent across all generated types. Here are my observations:

What works well

  • Using file-scoped types for MockImpl, MockFactory, and MockDelegateFactory is an excellent ergonomics improvement. These are pure implementation details and have no business leaking into user IntelliSense. The C# 11 file modifier is the right tool here.
  • Merging MockImpl and MockFactory into a single generated file (_MockImplFactory.g.cs) is a clean design: both types must be in the same file to reference each other, so co-locating them removes the need to coordinate across two separately emitted files.
  • Converting MockFactoryBuilder.Build() / MockImplBuilder.Build() from returning string to void BuildInto(CodeWriter writer, ...) and extracting BuildCombinedImplAndFactory into MockGenerator is a good separation of concerns: the builders no longer need to know about file headers or namespaces, and the generator owns the file structure.
  • Removing the // Generate constructor dispatch based on available constructors comment (now that the guard logic is in a named helper) keeps the code self-documenting.
  • Routing MockStaticExtension through Mock.Of<T>() instead of the now-invisible XxxMockFactory.Create(...) is the correct fix; the extension was the only remaining external caller that needed to be updated.

Potential issue: [ModuleInitializer] on internal static void Register() inside a file class

The [ModuleInitializer] attribute requires that the method be accessible from the module-level initializer, meaning it must be public or internal (C# spec §22.5; the Roslyn requirement is the method is "accessible from the module-level initializer"). Using internal on the Register() method inside a file-scoped class is technically valid — the compiler generates an internal-equivalent symbol scoped to the file — and the attribute does work here. The snapshot tests passing on net10.0 confirm this at runtime. However, it is worth noting this is slightly unusual; public would also be acceptable and might be more idiomatic for [ModuleInitializer] methods. Not a bug, but worth a comment if any reader ever wonders whether internal is intentional or a leftover from before file was added.

Observation: GenerateWrapMock now delegates entirely to GenerateImplFactoryMembersAndEvents

GenerateWrapMock is now a one-liner:

private static void GenerateWrapMock(SourceProductionContext spc, MockTypeModel model)
{
    var fileName = GetSafeFileName(model);
    GenerateImplFactoryMembersAndEvents(spc, model, fileName);
}

This is fine, but if GenerateWrapMock does nothing beyond computing fileName and forwarding, it could simply be inlined into the call site in Initialize. That said, keeping it as a named method improves readability at the dispatch site, so this is a style preference only.

Observation: Wrap_Mock_Filters_Internal_Virtual_Members_From_External_Assembly snapshot reordering

In the diff for this snapshot, the MockMemberExtensions class moved to the end of the file and the WrapMockImpl/WrapMockFactory pair moved to the top. This is a side-effect of combining impl+factory into one file emitted first, then the members builder emitted separately. The logical grouping (impl + factory together) is now better than the old ordering (factory in one file, impl in another), so the reorder is a net improvement.

Minor: BOM removal in some .verified.txt snapshots

Several verified files lost their leading UTF-8 BOM (// <auto-generated/>). This is a cosmetic change from the header generation in BuildCombinedImplAndFactory not emitting a BOM that the old per-builder approach apparently did. Not a functional issue, but worth being aware of if snapshot tooling (Verify.Net) ever re-adds it on regeneration.

Summary

The change is correct, consistent, and an improvement in all dimensions: better user experience (implementation types hidden from IntelliSense), fewer generated files to reason about, and cleaner builder APIs. The 746 tests passing and all 25 snapshots updated confirm the mechanical correctness. No blocking concerns.

@codacy-production
Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity

Metric Results
Complexity 0

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

@thomhurst thomhurst enabled auto-merge (squash) April 4, 2026 13:42
@thomhurst thomhurst merged commit ee14df3 into main Apr 4, 2026
15 checks passed
@thomhurst thomhurst deleted the refactor/mocks-file-scoped-generated-types branch April 4, 2026 14:02
@claude claude bot mentioned this pull request Apr 4, 2026
1 task
This was referenced Apr 4, 2026
This was referenced Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant